Описание проекта
Инвесторы из фонда «Shut Up and Take My Money» решили попробовать себя в новой области и открыть заведение общественного питания в Москве. Цель проекта: подготовить исследование рынка Москвы, найти интересные особенности и презентовать полученные результаты, которые в будущем помогут в выборе подходящего инвесторам места.
Описание данных
Нам доступен датасет с заведениями общественного питания Москвы, составленный на основе данных сервисов Яндекс Карты и Яндекс Бизнес на лето 2022 года. Информация, размещённая в сервисе Яндекс Бизнес, могла быть добавлена пользователями или найдена в общедоступных источниках. Она носит исключительно справочный характер.
Файл moscow_places.csv:
name — название заведения;address — адрес заведения;category — категория заведения, например «кафе», «пиццерия» или «кофейня»;hours — информация о днях и часах работы;lat — широта географической точки, в которой находится заведение;lng — долгота географической точки, в которой находится заведение;rating — рейтинг заведения по оценкам пользователей в Яндекс Картах (высшая оценка — 5.0);price — категория цен в заведении, например «средние», «ниже среднего», «выше среднего» и так далее;avg_bill — строка, которая хранит среднюю стоимость заказа в виде диапазона, например:middle_avg_bill — число с оценкой среднего чека, которое указано только для значений из столбца avg_bill, начинающихся с подстроки «Средний счёт»:middle_coffee_cup — число с оценкой одной чашки капучино, которое указано только для значений из столбца avg_bill, начинающихся с подстроки «Цена одной чашки капучино»:chain — число, выраженное 0 или 1, которое показывает, является ли заведение сетевым (для маленьких сетей могут встречаться ошибки):district — административный район, в котором находится заведение, например Центральный административный округ;seats — количество посадочных мест.# импортируем библиотеки
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import numpy as np
import re
import plotly.express as px
from plotly import graph_objects as go
import json
from folium import Map, Choropleth
from folium import GeoJsonTooltip
import os
from folium import Map, Marker
from folium.plugins import MarkerCluster
# убираем предупреждения
pd.options.mode.chained_assignment = None # default='warn'
from matplotlib.axes._axes import _log as matplotlib_axes_logger
matplotlib_axes_logger.setLevel('ERROR')
# установим максимальное количество отображающихся столбцов
pd.set_option('display.max_columns', None)
# установим максимальную ширину отображающихся столбцов
pd.set_option('max_colwidth', 120)
# установим максимальное количество отображающихся элементов серии
np.set_printoptions(threshold=50) # 50 заменить на np.inf, если хотим показать все элементы
data¶# считаем данные
try:
data = pd.read_csv('/datasets/moscow_places.csv')
except:
data = pd.read_csv('moscow_places.csv')
data.head(5)
| name | category | address | district | hours | lat | lng | rating | price | avg_bill | middle_avg_bill | middle_coffee_cup | chain | seats | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | WoWфли | кафе | Москва, улица Дыбенко, 7/1 | Северный административный округ | ежедневно, 10:00–22:00 | 55.878494 | 37.478860 | 5.0 | NaN | NaN | NaN | NaN | 0 | NaN |
| 1 | Четыре комнаты | ресторан | Москва, улица Дыбенко, 36, корп. 1 | Северный административный округ | ежедневно, 10:00–22:00 | 55.875801 | 37.484479 | 4.5 | выше среднего | Средний счёт:1500–1600 ₽ | 1550.0 | NaN | 0 | 4.0 |
| 2 | Хазри | кафе | Москва, Клязьминская улица, 15 | Северный административный округ | пн-чт 11:00–02:00; пт,сб 11:00–05:00; вс 11:00–02:00 | 55.889146 | 37.525901 | 4.6 | средние | Средний счёт:от 1000 ₽ | 1000.0 | NaN | 0 | 45.0 |
| 3 | Dormouse Coffee Shop | кофейня | Москва, улица Маршала Федоренко, 12 | Северный административный округ | ежедневно, 09:00–22:00 | 55.881608 | 37.488860 | 5.0 | NaN | Цена чашки капучино:155–185 ₽ | NaN | 170.0 | 0 | NaN |
| 4 | Иль Марко | пиццерия | Москва, Правобережная улица, 1Б | Северный административный округ | ежедневно, 10:00–22:00 | 55.881166 | 37.449357 | 5.0 | средние | Средний счёт:400–600 ₽ | 500.0 | NaN | 1 | 148.0 |
data.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 8406 entries, 0 to 8405 Data columns (total 14 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 name 8406 non-null object 1 category 8406 non-null object 2 address 8406 non-null object 3 district 8406 non-null object 4 hours 7870 non-null object 5 lat 8406 non-null float64 6 lng 8406 non-null float64 7 rating 8406 non-null float64 8 price 3315 non-null object 9 avg_bill 3816 non-null object 10 middle_avg_bill 3149 non-null float64 11 middle_coffee_cup 535 non-null float64 12 chain 8406 non-null int64 13 seats 4795 non-null float64 dtypes: float64(6), int64(1), object(7) memory usage: 919.5+ KB
Вывод
float64 и int, так и в текстовом формате objectprice и average_bill хранят категориальные данные, поэтому данные в этих столбцах представлены в текстовом формате object.Проверим наличие дублирующихся строк.
data[data.duplicated()]
| name | category | address | district | hours | lat | lng | rating | price | avg_bill | middle_avg_bill | middle_coffee_cup | chain | seats |
|---|
Строки-дубликаты (явные/полные дубликаты) отсутствуют.
Также проверим наличие неявных дубликатов в столбце name для сетевых заведений (для несетевых заведений сложно угадать, это неявный дубликат или совершенно другое заведение).
data.query('chain==1')['name'].sort_values().unique()
array(['1-я Креветочная', '10 Идеальных Пицц', '18 Грамм', ...,
'Яндекс Лавка', 'Яндекс.Лавка', 'Японская кухня'], dtype=object)
В стобце name встречаются разные названия одной и той же сети:
Переименуем их на уникальное название.
data['name'] = data['name'].replace(['Яндекс Лавка', 'Яндекс.Лавка'], 'Яндекс.Лавка')
data['name'] = data['name'].replace(['Чайхона № 1', 'Чайхона №1'], 'Чайхона №1')
data['name'] = data['name'].replace(['Чайхана Халаль', 'Чайхана Халяль', 'Чайхана халяль'], 'Чайхана Халяль')
data['name'] = data['name'].replace(["Домино'с Пицца", "Домино'с пицца", "Доминос пицца"], "Домино'с Пицца")
for column in data.columns:
if data[column].isna().sum()>0:
print("Название столбца:", column)
print("Количество пропусков:", data[column].isna().sum())
print('----')
print("В остальных столбцах пропуски отсутствуют")
Название столбца: hours Количество пропусков: 536 ---- Название столбца: price Количество пропусков: 5091 ---- Название столбца: avg_bill Количество пропусков: 4590 ---- Название столбца: middle_avg_bill Количество пропусков: 5257 ---- Название столбца: middle_coffee_cup Количество пропусков: 7871 ---- Название столбца: seats Количество пропусков: 3611 ---- В остальных столбцах пропуски отсутствуют
Пропуски в столбцах hours, price, avg_bill и seats скорее всего обозначает отсутствие данных о времени работы заведения, поэтому заполним пропуски значением no_info.
# заполняем пропуски и nan в столбцах hours, price, avg_bill и seats значением 'no_info'
data[['hours', 'price', 'avg_bill', 'seats']] = data[['hours', 'price', 'avg_bill', 'seats']].fillna('no_info')
Пропуски в столбце middle_avg_bill могут означать, что столбец avg_bill не начинаетя с подстроки «Средний счёт», а пропуски в столбце middle_coffee_cup - что столбец avg_bill не начинаетя с подстроки «Цена одной чашки капучино». Если эта догадка верная, заменим пропуски значением N/A - not applicable.
# проверим условие для столбца middle_avg_bill
if data[~data['avg_bill'].str.startswith("Средний счёт")]['middle_avg_bill'].isna().sum() == data['middle_avg_bill'].isna().sum():
print('Пропуски в столбце middle_avg_bill встречаются только в тех случаях, когда значения из столбца avg_bill не начинаются')
print('с подстроки «Средний счёт». Пропуски заменили значением N\A.')
# заменим пропуски на 'N\A'
data['middle_avg_bill'] = data['middle_avg_bill'].fillna('N/A')
else:
print('Догадка неверная.')
print('----')
# проверим условие для столбца middle_coffee_cup
if data[~data['avg_bill'].str.startswith("Цена одной чашки капучино")]['middle_coffee_cup'].isna().sum() == data['middle_coffee_cup'].isna().sum():
print('Пропуски в столбце middle_coffee_cup встречаются только в тех случаях, когда значения из столбца avg_bill не начинаются')
print('с подстроки «Цена одной чашки капучино». Пропуски заменили значением N\A.')
# заменим пропуски на 'N\A'
data['middle_coffee_cup'] = data['middle_coffee_cup'].fillna('N/A')
else:
print('Догадка неверная.')
Пропуски в столбце middle_avg_bill встречаются только в тех случаях, когда значения из столбца avg_bill не начинаются с подстроки «Средний счёт». Пропуски заменили значением N\A. ---- Пропуски в столбце middle_coffee_cup встречаются только в тех случаях, когда значения из столбца avg_bill не начинаются с подстроки «Цена одной чашки капучино». Пропуски заменили значением N\A.
street с названиями улиц из столбца с адресом¶data['street'] = data['address'].apply(lambda x: x.split(',')[1].strip())
data[['street']]
| street | |
|---|---|
| 0 | улица Дыбенко |
| 1 | улица Дыбенко |
| 2 | Клязьминская улица |
| 3 | улица Маршала Федоренко |
| 4 | Правобережная улица |
| ... | ... |
| 8401 | Профсоюзная улица |
| 8402 | Пролетарский проспект |
| 8403 | Люблинская улица |
| 8404 | Люблинская улица |
| 8405 | Россошанский проезд |
8406 rows × 1 columns
is_24_7 с обозначением, что заведение работает ежедневно и круглосуточно¶data['is_24_7'] = data['hours'] == 'ежедневно, круглосуточно'
data[['is_24_7']]
| is_24_7 | |
|---|---|
| 0 | False |
| 1 | False |
| 2 | False |
| 3 | False |
| 4 | False |
| ... | ... |
| 8401 | False |
| 8402 | False |
| 8403 | True |
| 8404 | True |
| 8405 | True |
8406 rows × 1 columns
distr_short с аббревиатурой для каждого района¶# Функция для преобразования значения в аббревиатуру
def get_initials(text):
words = text.replace('-', ' ').split()
initials = ""
for word in words:
initials += word[0].upper()
return initials
# Создание столбца с аббревиатурами
data['distr_short'] = data['district'].apply(get_initials)
data['distr_short'].unique()
array(['САО', 'СВАО', 'СЗАО', 'ЗАО', 'ЦАО', 'ВАО', 'ЮВАО', 'ЮАО', 'ЮЗАО'],
dtype=object)
Вывод
Итоговый датасет после предобработки данных и с новыми столбцами выглядит следующим образом.
data
| name | category | address | district | hours | lat | lng | rating | price | avg_bill | middle_avg_bill | middle_coffee_cup | chain | seats | street | is_24_7 | distr_short | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | WoWфли | кафе | Москва, улица Дыбенко, 7/1 | Северный административный округ | ежедневно, 10:00–22:00 | 55.878494 | 37.478860 | 5.0 | no_info | no_info | N/A | N/A | 0 | no_info | улица Дыбенко | False | САО |
| 1 | Четыре комнаты | ресторан | Москва, улица Дыбенко, 36, корп. 1 | Северный административный округ | ежедневно, 10:00–22:00 | 55.875801 | 37.484479 | 4.5 | выше среднего | Средний счёт:1500–1600 ₽ | 1550.0 | N/A | 0 | 4.0 | улица Дыбенко | False | САО |
| 2 | Хазри | кафе | Москва, Клязьминская улица, 15 | Северный административный округ | пн-чт 11:00–02:00; пт,сб 11:00–05:00; вс 11:00–02:00 | 55.889146 | 37.525901 | 4.6 | средние | Средний счёт:от 1000 ₽ | 1000.0 | N/A | 0 | 45.0 | Клязьминская улица | False | САО |
| 3 | Dormouse Coffee Shop | кофейня | Москва, улица Маршала Федоренко, 12 | Северный административный округ | ежедневно, 09:00–22:00 | 55.881608 | 37.488860 | 5.0 | no_info | Цена чашки капучино:155–185 ₽ | N/A | 170.0 | 0 | no_info | улица Маршала Федоренко | False | САО |
| 4 | Иль Марко | пиццерия | Москва, Правобережная улица, 1Б | Северный административный округ | ежедневно, 10:00–22:00 | 55.881166 | 37.449357 | 5.0 | средние | Средний счёт:400–600 ₽ | 500.0 | N/A | 1 | 148.0 | Правобережная улица | False | САО |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 8401 | Суши Мания | кафе | Москва, Профсоюзная улица, 56 | Юго-Западный административный округ | ежедневно, 09:00–02:00 | 55.670021 | 37.552480 | 4.4 | no_info | no_info | N/A | N/A | 0 | 86.0 | Профсоюзная улица | False | ЮЗАО |
| 8402 | Миславнес | кафе | Москва, Пролетарский проспект, 19, корп. 1 | Южный административный округ | ежедневно, 08:00–22:00 | 55.640875 | 37.656553 | 4.8 | no_info | no_info | N/A | N/A | 0 | 150.0 | Пролетарский проспект | False | ЮАО |
| 8403 | Самовар | кафе | Москва, Люблинская улица, 112А, стр. 1 | Юго-Восточный административный округ | ежедневно, круглосуточно | 55.648859 | 37.743219 | 3.9 | no_info | Средний счёт:от 150 ₽ | 150.0 | N/A | 0 | 150.0 | Люблинская улица | True | ЮВАО |
| 8404 | Чайхана Sabr | кафе | Москва, Люблинская улица, 112А, стр. 1 | Юго-Восточный административный округ | ежедневно, круглосуточно | 55.648849 | 37.743222 | 4.2 | no_info | no_info | N/A | N/A | 1 | 150.0 | Люблинская улица | True | ЮВАО |
| 8405 | Kebab Time | кафе | Москва, Россошанский проезд, 6 | Южный административный округ | ежедневно, круглосуточно | 55.598229 | 37.604702 | 3.9 | no_info | no_info | N/A | N/A | 0 | 12.0 | Россошанский проезд | True | ЮАО |
8406 rows × 17 columns
# Определяем порядок сортировки
sorted_data1 = data['category'].value_counts().sort_values(ascending=False).reset_index()
sorted_data1.columns = ['category', 'count']
# Задаем размер графика
plt.figure(figsize=(8, 6))
# Создаем график
graph1 = sns.barplot(x='category', y='count', data=sorted_data1)
# Поворачиваем подписи на 45 градусов
labels1 = graph1.get_xticklabels()
graph1.set_xticklabels(labels1, rotation=45)
# Добавляем заголовок и подписи осей
plt.title('Распределение заведений по категориям')
plt.xlabel('Категории заведений')
plt.ylabel('Количество')
# Отображаем график
plt.show()
Выводы
# Фильтруем только те заведения, где имеется информация о количестве стульев
sorted_data2 = data.query('seats != "no_info"')
# Сохранияем информацию о стульях в формате int
sorted_data2['seats'] = sorted_data2['seats'].astype(int)
# Определяем порядок сортировки
sorted_data2 = sorted_data2.groupby('category').agg({'seats': 'median'}).sort_values('seats', ascending=False).reset_index()
# Задаем размер графика
plt.figure(figsize=(8, 6))
# Создаем график
graph2 = sns.barplot(x='category', y='seats', data=sorted_data2)
# Поворачиваем подписи на 45 градусов
labels2 = graph2.get_xticklabels()
graph2.set_xticklabels(labels2, rotation=45)
# Добавляем значения над каждым столбцом
for index, row in sorted_data2.iterrows():
graph2.annotate('{:.0f}'.format(row['seats']), xy=(index, row['seats']), ha='center', va='bottom')
# Добавляем заголовок и подписи осей
plt.title('Медианное количество посадочных мест в заведениях')
plt.xlabel('Категории заведений')
plt.ylabel('Медианное количество мест')
# Отображаем график
plt.show()
Выводы
# готовим данные для графика
sorted_data3 = data['chain'].value_counts().sort_values(ascending=False).reset_index()
sorted_data3.columns = ['type', 'count']
sorted_data3
| type | count | |
|---|---|---|
| 0 | 0 | 5201 |
| 1 | 1 | 3205 |
# готовим данные для графика
sorted_data3['type2'] = sorted_data3['type']
sorted_data3['type2'] = sorted_data3['type2'].replace([0, 1], ['несетевое', 'сетевое'])
# строим круговую диаграмму
fig3 = go.Figure(data=[go.Pie(labels=sorted_data3['type2'], # указываем значения, которые появятся на метках сегментов
values=sorted_data3['count'], # указываем данные, которые отобразятся на графике
pull = [0.1, 0])]) # добавляем аргумент, который выделит тип-лидер на графике
fig3.update_layout(title='Соотношение сетевых и несетевых заведений', # указываем заголовок графика
width=700, # указываем размеры графика
height=500,
annotations=[dict(x=1.12, # вручную настраиваем аннотацию легенды
y=1.05,
text='Количество заведений',
showarrow=False)])
fig3.show() # выводим график
Вывод
# Подготовим данные для графика
sorted_data4 = data.groupby(['category', 'chain']).agg({'name': 'count'}).reset_index()
sorted_data4['chain'] = sorted_data4['chain'].replace([0, 1], ['несетевое', 'сетевое'])
sorted_data4.rename(columns={'name': 'count'}, inplace=True)
sorted_data4 = sorted_data4.sort_values('count', ascending=False).reset_index()
# Задаем размер графика
plt.figure(figsize=(10, 7))
# Создаем график
graph4 = sns.barplot(x='category', y='count', hue='chain', data=sorted_data4)
# Поворачиваем подписи на 45 градусов
labels4 = graph4.get_xticklabels()
graph4.set_xticklabels(labels4, rotation=45)
# Убираем надпись 'chain' из легенды
graph4.legend(title=None)
# Добавляем заголовок и подписи осей
plt.title('Категории сетевых и несетевых заведений')
plt.xlabel('Категории')
plt.ylabel('Количество заведений')
# Отображаем график
plt.show()
Вывод
Сетевыми заведениями чаще являются кофейни, пицерии и булочные. У остальных категорий заведений чаще встречаются несетевые типы.
# Подготовим данные для графика
sorted_data5 = data.query('chain==1')
sorted_data5 = sorted_data5.groupby(['name', 'category']).agg({'address': 'count'}).reset_index()
sorted_data5.rename(columns={'address': 'count'}, inplace=True)
sorted_data5 = sorted_data5.sort_values('count', ascending=False).reset_index().drop('index', axis=1)
sorted_data5 = sorted_data5.head(15)
sorted_data5
| name | category | count | |
|---|---|---|---|
| 0 | Шоколадница | кофейня | 119 |
| 1 | Домино'с Пицца | пиццерия | 76 |
| 2 | Додо Пицца | пиццерия | 74 |
| 3 | Яндекс.Лавка | ресторан | 72 |
| 4 | One Price Coffee | кофейня | 71 |
| 5 | Cofix | кофейня | 65 |
| 6 | Prime | ресторан | 49 |
| 7 | КОФЕПОРТ | кофейня | 42 |
| 8 | Кулинарная лавка братьев Караваевых | кафе | 39 |
| 9 | Теремок | ресторан | 36 |
| 10 | CofeFest | кофейня | 31 |
| 11 | Чайхана | кафе | 26 |
| 12 | Буханка | булочная | 25 |
| 13 | Drive Café | кафе | 24 |
| 14 | Кофемания | кофейня | 22 |
# строим столбчатую диаграмму
fig5 = px.bar(sorted_data5.sort_values(by='count', ascending=True), # загружаем данные и заново их сортируем
x='count', # указываем столбец с данными для оси X
y='name', # указываем столбец с данными для оси Y
text='count', # добавляем аргумент, который отобразит текст с информацией о количестве
hover_data=['category'] # добавляем аргумент, который отобразит категорию при наведении курсора
)
# оформляем график
fig5.update_layout(title='ТОП-15 популярных сетей по количеству заведений',
xaxis_title='Количество заведений',
yaxis_title='Название сети')
fig5.show() # выводим график
print('ТОП-15 популярных сетей по количеству заведений относятся к следующим категориям:', sorted_data5['category'].unique())
ТОП-15 популярных сетей по количеству заведений относятся к следующим категориям: ['кофейня' 'пиццерия' 'ресторан' 'кафе' 'булочная']
Выводы
# Подготовим данные для графика
sorted_data6 = data.groupby(['distr_short', 'category']).agg({'address': 'count'}).reset_index()
sorted_data6.rename(columns={'address': 'count'}, inplace=True)
sorted_data6 = sorted_data6.sort_values('count', ascending=False).reset_index().drop('index', axis=1)
sorted_data6_distr = sorted_data6.groupby(['distr_short']).agg({'count': 'sum'}).reset_index()
sorted_data6 = pd.merge(sorted_data6, sorted_data6_distr, on='distr_short')
sorted_data6.rename(columns={'count_x': 'count_cat', 'count_y': 'count_distr'}, inplace=True)
# Отсортируем данные
sorted_data6 = sorted_data6.sort_values(by=['count_distr', 'count_cat'], ascending=False)
# Создание столбчатой диаграммы
fig6 = px.bar(sorted_data6, x='distr_short', y='count_cat', color='category', labels={
'category': 'Категория заведения',
'distr_short': 'Административный район',
'count_cat': 'Количество заведений'
})
# Настройка внешнего вида диаграммы
fig6.update_layout(title='Количество заведений по районам и категориям',
xaxis_title='Административный район',
yaxis_title='Количество заведений')
# Установка размера графика
fig6.update_layout(width=950, height=600)
fig6.show()
print('В датасете присутствуют', data['district'].nunique(), 'административных районов:', data['district'].unique())
В датасете присутствуют 9 административных районов: ['Северный административный округ' 'Северо-Восточный административный округ' 'Северо-Западный административный округ' 'Западный административный округ' 'Центральный административный округ' 'Восточный административный округ' 'Юго-Восточный административный округ' 'Южный административный округ' 'Юго-Западный административный округ']
Выводы
# Определяем порядок сортировки
sorted_data7 = data.groupby('category').agg({'rating': 'median'}).sort_values('rating', ascending=False).reset_index()
# Задаем размер графика
plt.figure(figsize=(8, 6))
# Создаем график
graph7 = sns.barplot(x='category', y='rating', data=sorted_data7)
# Поворачиваем подписи на 45 градусов
labels7 = graph7.get_xticklabels()
graph7.set_xticklabels(labels7, rotation=45)
# Добавляем значения над каждым столбцом
for index, row in sorted_data7.iterrows():
graph7.annotate('{:.1f}'.format(row['rating']), xy=(index, row['rating']), ha='center', va='bottom')
# ограничиваем ось Y для наглядности
plt.ylim(3.8, 4.5)
# Добавляем заголовок и подписи осей
plt.title('Медианные рейтинги заведений')
plt.xlabel('Категории заведений')
plt.ylabel('Медианный рейтинг')
# Отображаем график
plt.show()
# Задаем размер графика
plt.figure(figsize=(9, 8))
# применяем стиль darkgrid из библиотеки seaborn
sns.set_style('darkgrid')
# строим график boxplot средствами seaborn
sns.boxplot(x='rating', y='category', data=data)
# указываем заголовок графика и подписи осей средствами matplotlib
plt.title('Распределение средней оценки в зависимости категории заведений')
plt.xlabel('Средняя оценка')
plt.ylabel('Категории заведений')
# отображаем график на экране
plt.show()
Выводы
В датасете представлено 9 округов.
Для каждого округа посчитаем медианный рейтинг торговых центров, которые находятся на его территории.
rating_data = data.groupby('district', as_index=False)['rating'].agg('median').sort_values(by='rating', ascending=False)
rating_data
| district | rating | |
|---|---|---|
| 5 | Центральный административный округ | 4.4 |
| 0 | Восточный административный округ | 4.3 |
| 1 | Западный административный округ | 4.3 |
| 2 | Северный административный округ | 4.3 |
| 4 | Северо-Западный административный округ | 4.3 |
| 7 | Юго-Западный административный округ | 4.3 |
| 8 | Южный административный округ | 4.3 |
| 3 | Северо-Восточный административный округ | 4.2 |
| 6 | Юго-Восточный административный округ | 4.2 |
# загружаем JSON-файл с границами округов Москвы
pth1 = '/datasets/admin_level_geomap.geojson'
pth2 = 'admin_level_geomap.geojson'
if os.path.exists(pth1):
state_geo = pth1
elif os.path.exists(pth2):
state_geo = pth2
else:
print('Something is wrong')
# moscow_lat - широта центра Москвы, moscow_lng - долгота центра Москвы
moscow_lat, moscow_lng = 55.751244, 37.618423
# создаём карту Москвы
m = Map(location=[moscow_lat, moscow_lng], zoom_start=10)
# создаём хороплет с помощью конструктора Choropleth и добавляем его на карту
Choropleth(
geo_data=state_geo,
data=rating_data,
columns=['district', 'rating'],
key_on='feature.name',
fill_color='YlGn',
fill_opacity=0.8,
legend_name='Медианный рейтинг заведений по районам',
).add_to(m)
# создаём пустой кластер, добавляем его на карту
marker_cluster = MarkerCluster().add_to(m)
# пишем функцию, которая принимает строку датафрейма,
# создаёт маркер в текущей точке и добавляет его в кластер marker_cluster
def create_clusters(row):
Marker(
[row['lat'], row['lng']],
popup=f"{row['name']} {row['rating']}",
).add_to(marker_cluster)
# применяем функцию create_clusters() к каждой строке датафрейма
data.apply(create_clusters, axis=1)
# выводим карту
m
sorted_data8 = data.groupby(['street', 'category']).agg({'address': 'count'}).reset_index()
sorted_data8.rename(columns={'address': 'count'}, inplace=True)
sorted_data8 = sorted_data8.sort_values('count', ascending=False).reset_index().drop('index', axis=1)
top_15_str = sorted_data8.groupby(['street']).agg({'count': 'sum'}).sort_values('count', ascending=False).reset_index().head(15)
top_15_str
| street | count | |
|---|---|---|
| 0 | проспект Мира | 184 |
| 1 | Профсоюзная улица | 122 |
| 2 | проспект Вернадского | 108 |
| 3 | Ленинский проспект | 107 |
| 4 | Ленинградский проспект | 95 |
| 5 | Дмитровское шоссе | 88 |
| 6 | Каширское шоссе | 77 |
| 7 | Варшавское шоссе | 76 |
| 8 | Ленинградское шоссе | 70 |
| 9 | МКАД | 65 |
| 10 | Люблинская улица | 60 |
| 11 | улица Вавилова | 55 |
| 12 | Кутузовский проспект | 54 |
| 13 | улица Миклухо-Маклая | 49 |
| 14 | Пятницкая улица | 48 |
# Подготовим данные для графика
sorted_data8 = sorted_data8[sorted_data8['street'].isin(top_15_str['street'])]
sorted_data8 = pd.merge(sorted_data8, top_15_str, on='street')
sorted_data8.rename(columns={'count_x': 'count_cat', 'count_y': 'count_str'}, inplace=True)
# Отсортируем данные
sorted_data8 = sorted_data8.sort_values(by=['count_str', 'count_cat'], ascending=False)
# Создание столбчатой диаграммы
fig8 = px.bar(sorted_data8, x='street', y='count_cat', color='category', labels={
'category': 'Категория заведения',
'street': 'Улица',
'count_cat': 'Количество заведений'
})
# Настройка внешнего вида диаграммы
fig8.update_layout(title='ТОП-15 улиц по количеству заведений',
xaxis_title='Улица',
yaxis_title='Количество заведений')
fig8.show()
Выводы
# Выявляем улицы, где есть только 1 заведение
sorted_data9 = data.groupby(['street']).agg({'address': 'count'}).query('address==1').reset_index()
print('Всего улиц, на которых находится только один объект общепита:', sorted_data9['street'].nunique())
print()
print(sorted_data9['street'].unique())
Всего улиц, на которых находится только один объект общепита: 458 ['1-й Автозаводский проезд' '1-й Балтийский переулок' '1-й Варшавский проезд' ... 'улица Шухова' 'улица Юннатов' '№ 7']
Посмотрим, какие заведения расположены на таких улицах.
# Фильтруем таблицу data, чтобы оставить только улицы, где есть только 1 заведение
sorted_data10 = pd.merge(sorted_data9, data, on='street')
print('Всего', sorted_data10['name'].nunique(), 'заведений являются единственными заведениями на улице.')
print('----')
print('Из них', sorted_data10.query('chain==1')['name'].count(), 'являются сетевыми заведениями.')
print('----')
print('Список заведения, которые являются единственными заведениями на улице:')
print()
print(sorted_data10['name'].unique())
Всего 403 заведений являются единственными заведениями на улице. ---- Из них 133 являются сетевыми заведениями. ---- Список заведения, которые являются единственными заведениями на улице: ['Чайхана Азия' 'Хуан Хэ' 'Колизей' ... 'Мираж' 'Scirocco' 'Енот']
Вывод
Для каждого округа посчитаем медианное значение стоимости заказа в заведениях, которые находятся на его территории.
# Фильтруем только те строки, где у нас есть числовые значения для заказа
bill_data = data.query('middle_avg_bill != "N/A"')
# Переводим в числовой формат
bill_data['middle_avg_bill'] = pd.to_numeric(bill_data['middle_avg_bill'])
# Готовим данные для отражения на карте
bill_data = bill_data.groupby('district', as_index=False)['middle_avg_bill'].agg('median')\
.sort_values(by='middle_avg_bill', ascending=False).reset_index().drop('index', axis=1)
bill_data
| district | middle_avg_bill | |
|---|---|---|
| 0 | Западный административный округ | 1000.0 |
| 1 | Центральный административный округ | 1000.0 |
| 2 | Северо-Западный административный округ | 700.0 |
| 3 | Северный административный округ | 650.0 |
| 4 | Юго-Западный административный округ | 600.0 |
| 5 | Восточный административный округ | 575.0 |
| 6 | Северо-Восточный административный округ | 500.0 |
| 7 | Южный административный округ | 500.0 |
| 8 | Юго-Восточный административный округ | 450.0 |
Изобразим полученные значения на карте Москвы, построим фоновую картограмму.
# создаём карту Москвы
m2 = Map(location=[moscow_lat, moscow_lng], zoom_start=10)
# создаём хороплет с помощью конструктора Choropleth и добавляем его на карту
Choropleth(
geo_data=state_geo,
data=bill_data,
columns=['district', 'middle_avg_bill'],
key_on='feature.name',
fill_color='PuBu',
fill_opacity=0.8,
legend_name='Медианная стоимость заказа',
).add_to(m2)
# выводим карту
m2
Вывод
Основателям фонда «Shut Up and Take My Money» не даёт покоя успех сериала «Друзья». Их мечта — открыть такую же крутую и доступную, как «Central Perk», кофейню в Москве. Заказчики не боятся конкуренции в этой сфере, ведь кофеен в больших городах уже достаточно. Попробуем проанализировать, осуществима ли эта мечта.
Кофейню потенциально можно расположить в двух местах: 1) на улице, где отсутствуют другие кофейни, и конкуренция
2) на улице, где существует большая концентрация кофеен, и, соответственно,потребители специально туда едут за кофе из-за наличия большого выбора (при этом надо учитывать, чтобы на этой улице отсутствовали сетевые кофейни, так как их потребители будут лояльны бренду)
Для начала посмотрим, как количество кофеен меняется в зависимости от района.
# Подготовим данные для графика
sorted_data11 = sorted_data6.query('category=="кофейня"').sort_values(by='count_cat', ascending=False)
# Создание столбчатой диаграммы
fig11 = go.Figure()
# Добавление столбцов
fig11.add_trace(go.Bar(x=sorted_data11['distr_short'][:-1], y=sorted_data11['count_cat'][:-1],
marker=dict(color='rgba(0, 0, 255, 1)'), # Можете выбрать свой цвет для столбцов
hovertemplate='<br>'.join([
'Административный район: %{x}',
'Количество кофеен: %{y}'
])
))
fig11.add_trace(go.Bar(x=sorted_data11['distr_short'][-1:], y=sorted_data11['count_cat'][-1:],
marker=dict(color='rgba(255, 0, 0, 1.0)'), # Можете выбрать свой цвет для последнего столбца
hovertemplate='<br>'.join([
'Административный район: %{x}',
'Количество кофеен: %{y}'
])
))
# Настройка внешнего вида диаграммы
fig11.update_layout(
title={
'text': 'Количество кофеен в районах Москвы',
'font': {'size': 20}
},
xaxis_title={
'text': 'Административный район',
'font': {'size': 16}
},
yaxis_title={
'text': 'Количество кофеен',
'font': {'size': 16}
},
font=dict(
size=15
), showlegend=False)
# Установка размера графика
fig11.update_layout(width=950, height=600)
fig11.show()
Вывод
data_streets_0¶Выделим те улицы СЗАО, где отсутствуют другие кофейни, и конкуренция.
# группируем данные по улицам и категориям, считаем количество заведений
sorted_data12 = data.groupby(['street', 'category']).agg({'address': 'count'}).reset_index()
# фильтруем улицы, на которых нет категории "кофейня"
sorted_data12 = sorted_data12.groupby('street').filter(lambda x: 'кофейня' not in x['category'].values)['street'].unique()
# создаем таблицу с информацией об улицах СЗАО , где отсутствует кофейня
data_streets_0 = data[data['street'].isin(sorted_data12)].reset_index().drop('index', axis=1)
data_streets_0 = data_streets_0.query('distr_short=="СЗАО"').reset_index().drop('index', axis=1)
data_streets_0['street'].unique()
array(['Парусный проезд', 'Походный проезд', 'Лодочная улица',
'улица Мещерякова', 'бульвар Яна Райниса', 'улица Фомичёвой',
'улица Фабрициуса', 'улица Василия Петушкова',
'улица Вилиса Лациса', 'Строительный проезд', 'проезд Донелайтиса',
'Новопоселковая улица', 'Таллинская улица', 'улица Твардовского',
'Одинцовская улица', 'улица Исаковского',
'памятник природы Серебряный бор',
'4-я линия Хорошёвского Серебряного Бора', 'улица Маршала Рыбалко',
'улица Максимова', 'улица Маршала Соколовского',
'улица Маршала Тухачевского', 'улица Расплетина',
'улица Академика Бочвара', 'Живописная улица',
'Иваньковское шоссе', '1-я линия Хорошёвского Серебряного Бора',
'Большой Волоколамский проезд', 'улица Паршина',
'Новохорошёвский проезд', 'улица Маршала Новикова',
'улица Генерала Глаголева', 'улица Водников', 'улица Рогова',
'улица Демьяна Бедного', 'улица Саляма Адиля',
'1-й Силикатный проезд', 'Причальный проезд',
'2-й Силикатный проезд'], dtype=object)
data_streets_1¶# фильтруем данные и группуируем по улице
sorted_data18 = data.query('category=="кофейня" and distr_short=="СЗАО" and chain==0')\
.groupby(['street']).agg({'address': 'count'}).reset_index()
# оставляем названия улицы СЗАО с большой концентрацией несетевых кофеен (2 и более)
sorted_data18 = sorted_data18.query('address>=2').reset_index().drop('index', axis=1)['street'].unique()
# создаем новый датафрейм со всеми данными из data, где будут улицы СЗАО с большой концентрацией несетевых кофеен
data_streets_1 = data[data['street'].isin(sorted_data18)].reset_index().drop('index', axis=1)
data_streets_1 = data_streets_1.query('category=="кофейня" and chain==0').reset_index().drop('index', axis=1)
print('улицы СЗАО с большой концентрацией несетевых кофеен (2 и более):')
print()
print(data_streets_1['street'].unique())
улицы СЗАО с большой концентрацией несетевых кофеен (2 и более): ['Сходненская улица' 'улица Маршала Катукова' 'бульвар Генерала Карбышева' 'Шелепихинская набережная']
# Подготовим данные для графика
sorted_data17 = data_streets_1.groupby(['street']).agg({'address': 'count'}).reset_index()
# Отсортируем данные
sorted_data17 = sorted_data17.sort_values(by=['address'], ascending=True)
# Создание столбчатой диаграммы
fig17 = px.bar(sorted_data17, y='street', x='address', labels={
'street': 'Улица',
'address': 'Количество кофеен'
})
# Настройка внешнего вида диаграммы
fig17.update_layout(title={
'text': 'Улицы СЗАО с большой концентрацией несетевых кофеен',
'font': {'size': 20}
},
xaxis_title={
'text': 'Количество кофеен',
'font': {'size': 16}
},
yaxis_title={
'text': 'Улица',
'font': {'size': 16}
})
fig17.update_xaxes(tickfont=dict(size=14), dtick=1)
fig17.update_yaxes(tickfont=dict(size=14))
fig17.show()
Вывод
Мы выделили улицы СЗАО, где лучше расположить кофейню. Проанализируем другие критерии, чтобы сузить выборку улиц.
Проанализируем, много ли в Москве круглосуточных кофеен.
# готовим данные для графика
sorted_data13 = data.query('category=="кофейня"')['is_24_7'].value_counts().sort_values(ascending=False).reset_index()
sorted_data13.columns = ['is_24_7', 'count']
sorted_data13
| is_24_7 | count | |
|---|---|---|
| 0 | False | 1354 |
| 1 | True | 59 |
# готовим данные для графика - переименуем булевые значения
sorted_data13['is_24_7'] = sorted_data13['is_24_7'].replace([False, True], ['не круглосуточные', 'круглосуточные'])
# строим круговую диаграмму
fig13 = go.Figure(data=[go.Pie(labels=sorted_data13['is_24_7'], # указываем значения, которые появятся на метках сегментов
values=sorted_data13['count'], # указываем данные, которые отобразятся на графике
pull = [0.1, 0])]) # добавляем аргумент, который выделит тип-лидер на графике
fig13.update_layout(title='Соотношение круглосуточных и не круглосуточных кофеен', # указываем заголовок графика
width=830, # указываем размеры графика
height=500,
annotations=[dict(x=1.12, # вручную настраиваем аннотацию легенды
y=1.05,
text='Количество кофеен',
showarrow=False)],
font=dict(size=13))
fig13.show() # выводим график
Вывод
в Москве доля круглосуточных кофеен крайне низкая - 4.18%; скорее всего, это означает, что при прочих равных держать круглосуточную кофейню не выгодно (круглосуточными лучше делать бары/пабы)
делать круглосуточные кафе имеет смысл только чтобы заработать конкурентное преимущество в местах с большой концентрацией кофеен.
data_streets_1 на наличие круглосуточных кофеен¶data_streets_1.query('is_24_7==True')
| name | category | address | district | hours | lat | lng | rating | price | avg_bill | middle_avg_bill | middle_coffee_cup | chain | seats | street | is_24_7 | distr_short |
|---|
Вывод
Посмотрим, как рейтинг кофеен меняются в зависимости от района.
# Определяем порядок сортировки
sorted_data14 = data.query('category=="кофейня"').groupby('distr_short').agg({'rating': 'median'}).sort_values('rating', ascending=False).reset_index()
# Задаем размер графика
plt.figure(figsize=(8, 6))
# Создаем график
graph14 = sns.barplot(x='distr_short', y='rating', data=sorted_data14)
# Поворачиваем подписи на 45 градусов
labels14 = graph14.get_xticklabels()
graph14.set_xticklabels(labels14, rotation=45)
# Добавляем значения над каждым столбцом
for index, row in sorted_data14.iterrows():
graph14.annotate('{:.1f}'.format(row['rating']), xy=(index, row['rating']), ha='center', va='bottom')
# ограничиваем ось Y для наглядности
plt.ylim(4, 4.4)
# Добавляем заголовок и подписи осей
plt.title('Медианные рейтинги кофеен')
plt.xlabel('Административные районы')
plt.ylabel('Медианный рейтинг')
# Отображаем график
plt.show()
Вывод
data_streets_1 (улицы с большой концентрацией несетевых кофеен)¶# определяем порядок сортировки
sorted_data15 = data_streets_1.groupby('street').agg({'rating': 'median'}).sort_values('rating', ascending=False).reset_index()
sorted_data15['rating'] = sorted_data15['rating'].apply(lambda x: round(x, 2))
# строим столбчатую диаграмму
fig15 = px.bar(sorted_data15.sort_values(by='rating', ascending=True), # загружаем данные и заново их сортируем
x='rating', # указываем столбец с данными для оси X
y='street', # указываем столбец с данными для оси Y
text='rating', # добавляем аргумент, который отобразит текст с информацией о количестве
hover_data=['rating'] # добавляем аргумент, который отобразит категорию при наведении курсора
)
# оформляем график
fig15.update_layout(title='Рейтинги кофеен на улицах с большой концентрацией несетевых кофеен',
xaxis_title='Медианный рейтинг',
yaxis_title='Улица',
font=dict(size=13)) # увеличиваем размер шрифта
# красим нижний столбец в красный
fig15.update_traces(marker=dict(color=['red' if x == sorted_data15['rating'].max() else 'blue' for x in sorted_data15['rating']]))
fig15.update_xaxes(range=[3.6, 4.8])
fig15.show() # выводим график
Вывод
На улице Маршала Катукова медианный пользовательский рейтинг кофеен самый низкий среди ТОП-4 улиц СЗАО по концентрации несетевых кофеен. На этой улице есть возможность открыть кафе и навязать конкуренцию.
data_streets_0 (улицы, где отсутствуют кофейни)¶Так как мы до этого выявили много улиц СЗАО, где отсутствуют кофейни, сразу поставим фильтр на средний рейтинг заведений меньше 4 и покажем оставшиеся улицы на графике.
# определяем порядок сортировки
sorted_data16 = data_streets_0.groupby('street').agg({'rating': 'median'}).query('rating<4').sort_values('rating', ascending=False).reset_index()
# ручная корректировка самого крупного значения, чтобы максимум был только один (нужно, чтобы удалось закрасить только один элемент)
sorted_data16.loc[0, 'rating'] = sorted_data16.loc[0, 'rating']+0.01
sorted_data16['rating'] = sorted_data16['rating'].apply(lambda x: round(x, 2))
# строим столбчатую диаграмму
fig16 = px.bar(sorted_data16.sort_values(by='rating', ascending=True), # загружаем данные и заново их сортируем
x='rating', # указываем столбец с данными для оси X
y='street', # указываем столбец с данными для оси Y
text='rating', # добавляем аргумент, который отобразит текст с информацией о количестве
hover_data=['rating'] # добавляем аргумент, который отобразит категорию при наведении курсора
)
# оформляем график
fig16.update_layout(title='Медианные рейтинги заведений на улицах без кофеен',
xaxis_title='Медианный рейтинг',
yaxis_title='Улица',
font=dict(size=13)) # увеличиваем размер шрифта
# красим нижний столбец в красный
fig16.update_traces(marker=dict(color=['red' if x == sorted_data16['rating'].max() else 'blue' for x in sorted_data16['rating']]))
fig16.update_xaxes(range=[2, 4])
fig16.show() # выводим график
Вывод
В Новохорошевском проезде отсутствуют кофейни, а заведения общепита, которые там есть, получили низкий рейтинг от посетителей. Следовательно, кофейню лучше открыть там.
Проанализируем медианную стоимость чашки капучино в Москве: для каждого округа посчитаем медианное значение стоимости капучино в кофейнях, которые находятся на его территории.
# Фильтруем только те строки, где у нас есть числовые значения для заказа
cof_data = data.query('middle_coffee_cup != "N/A" and category=="кофейня"')
# Переводим в числовой формат
cof_data['middle_coffee_cup'] = pd.to_numeric(cof_data['middle_coffee_cup'])
# Готовим данные для отражения на карте
cof_data = cof_data.groupby('district', as_index=False)['middle_coffee_cup'].agg('median')\
.sort_values(by='middle_coffee_cup', ascending=False).reset_index().drop('index', axis=1)
cof_data
| district | middle_coffee_cup | |
|---|---|---|
| 0 | Юго-Западный административный округ | 198.0 |
| 1 | Центральный административный округ | 190.0 |
| 2 | Западный административный округ | 189.0 |
| 3 | Северо-Западный административный округ | 165.0 |
| 4 | Северо-Восточный административный округ | 162.5 |
| 5 | Северный административный округ | 159.0 |
| 6 | Южный административный округ | 150.0 |
| 7 | Юго-Восточный административный округ | 147.5 |
| 8 | Восточный административный округ | 135.0 |
Изобразим полученные значения на карте Москвы, построим фоновую картограмму.
# создаём карту Москвы
m3 = Map(location=[moscow_lat, moscow_lng], zoom_start=10)
# создаём хороплет с помощью конструктора Choropleth и добавляем его на карту
Choropleth(
geo_data=state_geo,
data=cof_data,
columns=['district', 'middle_coffee_cup'],
key_on='feature.name',
fill_color='Oranges',
fill_opacity=0.8,
legend_name='Медианная стоимость капучино'
).add_to(m3)
# выводим карту
m3
Вывод
Цена на капучино заметно отличается в зависимости от района Москвы. Так как кофейня будет расположена в СЗАО, при открытии стоит ориентироваться на стоимость равную 165 руб.
Рекомендуется открыть кофейню в СЗАО на Новохорошевском проезде или на улице Маршала Катукова.
При открытии кофейни стоит ориентироваться на медианную стоимость капучино в СЗАО - 165 руб.
Презентация: https://disk.yandex.ru/i/dJzZAFEZeNBPHw